#
# (c) 2006 Travis F Vachon
#
# This software is distributable under the terms of the GNU
# General Public License (GPL) v2, the text of which can be found at
# http://www.gnu.org/copyleft/gpl.html. Installing, importing or otherwise
# using this module constitutes acceptance of the terms of this License.
#
# Disclaimer
# 
# This software is provided "as-is".  There are no expressed or implied
# warranties of any kind, including, but not limited to, the warranties
# of merchantability and fittness for a given application.  In no event
# shall Gary Strangman be liable for any direct, indirect, incidental,
# special, exemplary or consequential damages (including, but not limited
# to, loss of use, data or profits, or business interruption) however
# caused and on any theory of liability, whether in contract, strict
# liability or tort (including negligence or otherwise) arising in any way
# out of the use of this software, even if advised of the possibility of
# such damage.
#
import xmlrpclib
import socket
import threading
import sys
import time
from conf import *


updaters_running = True
updaters = []
hostname = socket.gethostname()
peer_list = node_list[:]
peer_nodes = {}
#Stupid way to not query ourself.
try:
    peer_list.remove(hostname)
except:
    print "Could not determine self."



class Node(object):
    '''
    A class to encapsulate information about a migration partner
    '''
    def __init__(self, node, memory_size=10):
        self.server = xmlrpclib.ServerProxy("http://"+node+":"+str(XENBAL_PORT),allow_none = True)

        self.cpu_loads = []
        self.net_load = 0
        self.name = node
        self.memory_size = memory_size

        self.domains = {}
        self.domain_loads = {}
        '''A dictionary in the form of domain:domain_load'''

        self._physinfo = None
        '''
        Physical information about this node
        '''


        #variables for the simulate_migration function
        self.total_loss = 0.0
        self.highest_loss = 0.0
        self.lowest_loss = None

        
        try:
            self._physinfo = self.server.physinfo()
        except:
            print "Could not contact XMLRPC server to initialize node."

    def get_physinfo(self):
        '''
        Return dictionary of physical information about node.

        Return None if infomation cannot be retrieved from the server
        '''
        if self._physinfo == None:
            try:
                self._physinfo = self.server.physinfo()
            except:
                print "Could not contact XMLRPC server to update physinfo."
                raise
        return self._physinfo

    def update_physinfo(self):
        try:
            self._physinfo = self.server.physinfo()
        except:
            print "Could not contact XMLRPC server to update physinfo."
        
    def is_fully_loaded(self, memory = 1):
        if self.get_cpu_load(memory) > FULLY_LOADED:
            return True
        else:
            return False
        

    def update_load_info(self):
        '''
        Made to be called periodically. LOAD INFORMATION IS ONLY UPDATED
        WITH THIS FUNCTION!!
        '''
        total_cpu_load = 0
        try:
            self.domain_loads = self.server.get_load(OPT_LOAD_TIME)
            
        except:
            print "Couldn't query " + self.name

            return False

        for domain in self.domains.values():
            domain.found = False

        for domain,load in self.domain_loads.items():
            if not self.domains.has_key(domain):
                migratable = True
                if str(domain) == '0':
                    migratable = False

                self.domains[domain] = Domain(self, domain,10, migratable)
            self.domains[domain].update_load(load)
            self.domains[domain].found = True
                
            total_cpu_load += load

        #Clean up any domains no longer on the node
        for domain in self.domains.keys():
            if self.domains[domain].found == False:
                self.domains.pop(domain)

        self.cpu_loads.insert(0,total_cpu_load)
        while len(self.cpu_loads) > self.memory_size:
            del self.cpu_loads[-1]
        return True

        
        
    def get_cpu_load_raw(self, memory=1):
        '''
        Get average cpu load over last "memory" (the variable) checks
        
        '''

        counter = 0
        cpu_load = 0.0
        if len(self.cpu_loads) == 0:
            counter = 1
        for load in self.cpu_loads:
            if counter == memory:
                break
            else:
                cpu_load += load
                counter  += 1


        cpu_load = cpu_load / counter

        return cpu_load

    def get_cpu_load(self, memory=1):
        load = self.get_cpu_load_raw(memory)
        return load/OPT_LOAD_TIME_N

    def simulate_migration(self,mig_domain, destination, load_memory = 1):
        '''Calculate the current loss in the system after a migration

        If mig_domain or destination is None, returns the current loss
        in the system.
        '''
        self.total_loss = 0.0
        self.highest_loss = 0.0
        self.lowest_loss = None
        
        if (not mig_domain == None) and (len(mig_domain.host.domains) < 3):
            mig_domain = None

        def update_loss_stats(loss):

            if loss > self.highest_loss:
                self.highest_loss = loss
            if self.lowest_loss == None or loss < self.lowest_loss:
                self.lowest_loss = loss
            self.total_loss += loss



        #Search through all domains. Note that "this node" is not
        #neccasarily "self"
        for node in peer_nodes.values() + [this_node]:

            if mig_domain == None or destination == None:
                for domain in node.domains.values():
                    if str(domain.id) != '0':
                        update_loss_stats(domain.calc_loss(load_memory))
            
            elif node is destination:
                ave_loss = (node.get_physinfo()['cpu_khz'] 
                            * (1.0-(0.90/len(node.domains))))
                            #one more than current # of guests

                for domain in node.domains.values():
                    if str(domain.id) != '0':
                        update_loss_stats(ave_loss)
                            
            elif node is mig_domain.host:
                ave_loss = (node.get_physinfo()['cpu_khz'] 
                            * (1.0-(0.90/(len(node.domains)-2))))
                            #one less than current # of guests

                for domain in node.domains.values():
                    if str(domain.id) != '0':
                        update_loss_stats(ave_loss)


            else:
                for domain in node.domains.values():
                    if str(domain.id) != '0':
                        update_loss_stats(domain.calc_loss(load_memory))
                
            
                
        #Calculate the current fairness 
        unfairness = self.highest_loss - self.lowest_loss
        
        return {'loss':self.total_loss, 'unfairness':unfairness}

    def get_max_load_domain(self, load_memory = 1):
        max_dom = None
        max_load = 0
        for dom in self.domains.values():
            if not str(dom.id) == '0':
                load = dom.get_cpu_load(load_memory)
                if load > max_load:
                    max_dom = dom
                    max_load = load

        return (max_dom, max_load)

    def get_min_load_domain(self, load_memory = 1):
        min_dom = None
        min_load = None
        for dom in self.domains.values():
            if not str(dom.id) == '0':
                load = dom.get_cpu_load(load_memory)
                if min_load == None or load < min_load:
                    min_dom = dom
                    min_load = load
        return (min_dom, min_load)

    
    def get_max_load_peer(self, load_memory = 1):
        max_node = None
        max_load = 0
        for node in peer_nodes.values():
            load = node.get_cpu_load(load_memory)
            if load > max_load:
                max_node = node
                max_load = load

        return (max_node, max_load)

    def get_min_load_peer(self, load_memory = 1):
        min_node = None
        min_load = None
        for node in peer_nodes.values():
            load = node.get_cpu_load(load_memory)
            if min_load == None or load < min_load:
                min_node = node
                min_load = load

        return (min_node, min_load)

    def get_sorted_peer_load_list(self, load_memory = 1):
        #Decorate, sort, undecorate. Awesome.
        peer_list = [(node.get_cpu_load(load_memory), node) for node in peer_nodes.values()+[this_node] if not node is self]
        peer_list.sort()
        return [peer[1] for peer in peer_list]
            



class Domain(object):
    '''
    A class to encapsulate information about a domain on this node
    '''
    def __init__(self,host, dom_id, memory_size = 10, migratable = True):
        self.host = host
        self.id = dom_id
        self.memory_size = memory_size
        self.migratable = migratable
        self.cpu_loads = []
        self._migrating = False

        

        #used when updating
        self._found = False

    def is_migrating(self):
        return self._migrating

    def set_migrating(self):
        self._migrating = True

    def update_load(self,new_cpu_load):
        self.cpu_loads.insert(0, new_cpu_load)
        while len(self.cpu_loads) > self.memory_size:
            del self.cpu_loads[-1]


    def get_cpu_load_raw(self, memory=1):
        '''
        Get average cpu load over last "memory" (the variable) checks
        
        '''
        
        counter = 0
        cpu_load = 0.0
        for load in self.cpu_loads:
            if counter == memory:
                break
            else:
                cpu_load += load
                counter += 1
        cpu_load = cpu_load / counter
        return cpu_load

    def get_cpu_load(self, memory=1):
        load = self.get_cpu_load_raw(memory)
        return load/OPT_LOAD_TIME_N
    
    def calc_loss(self, memory=1):
        """
        domain - globals.Domain object to calculate this domain's loss in
        potential productivity in cpu cycles.
        """
        
        node_load = self.host.get_cpu_load(memory)
        domain_load = self.get_cpu_load(memory)

        if node_load < FULLY_LOADED:
            #If the node is not fully loaded, return 0
            return 0.0
        else:
            #otherwise, return the lost potential in this domain
            percent_loss = node_load - domain_load
            return self.host.get_physinfo()['cpu_khz']*percent_loss




this_node = Node(hostname)

peer_nodes = {}
for node in peer_list:
    peer_nodes[node] = Node(node)


class Updater(threading.Thread):
    def __init__(self,node,wait):
        threading.Thread.__init__(self)
        self.node = node
        self.wait = wait
        self.running = False
    def run(self):
        self.running = True
        while(self.running):

            self.node.update_load_info()
            time.sleep(self.wait)
        
#start updaters
def create_updaters():
    updaters.append(Updater(this_node,UPDATER_WAIT_TIME))
    for node in peer_list:
        updaters.append(Updater(peer_nodes[node],UPDATER_WAIT_TIME))

    for updater in updaters:
        updater.start()

def stop_updaters():
    for updater in updaters:
        updater.running = False
    return True


def update_all_nodes():
    for node in peer_nodes.values() + [this_node]:
        node.update_load_info()
